package ru.mirent.perfomancer;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * Класс выполнения задачи и сбора статистики по ее работе.
 */
public class Statistics {
    /**
     * Константа для перевода байт в мегаайты.
     */
    private static final long MEGABYTE = 1024L * 1024L;

    /**
     * Количество повторений задания.
     */
    private final int numberOfRepetitions;

    /**
     * Список с временем запуска и окончания работы задачи.
     */
    private final Map<Long, Long> times = new HashMap<>();

    /**
     * Объект работы с JVM.
     */
    private final Runtime runtime = Runtime.getRuntime();

    /**
     * Создать экземпляр класса статистики
     * @param numberOfRepetitions количество повторений выполнения задачи в текущем запуске
     */
    public Statistics(int numberOfRepetitions) {
        this.numberOfRepetitions = numberOfRepetitions;
    }

    /**
     * Выполнить действие для проверки производительности.
     * @param action выполняемое действие
     * @return текущий объект
     */
    public Statistics perfomance(JobSupplier action) {
        runGagbageCollector();
        showTitle();

        for (int i = 0; i < numberOfRepetitions; i++) {
            long starrtTime = System.nanoTime();

            action.doIt();

            long endTime = System.nanoTime();
            times.put(starrtTime, endTime);

            showMemoryDump();
        }

        return this;
    }

    /**
     * Отобразить статистику.
     */
    public void showStatistics() {
        long totalElapsedTime = 0;

        for (Map.Entry<Long, Long> entry: times.entrySet()) {
            totalElapsedTime += entry.getValue() - entry.getKey();
        }

        System.out.printf("| %-20s | %6s      |%n", "Количество циклов", numberOfRepetitions);
        showMessage("Общее время", totalElapsedTime);
        if (!times.isEmpty()) {
            long averageElapsedTime = totalElapsedTime / times.size();
            showMessage("Среднее время", averageElapsedTime);
        }
    }

    /**
     * Отобразить начальный заголовок.
     */
    private void showTitle() {
        System.out.printf("|------------------------------------|%n");
        System.out.printf("|     Использование памяти между     |%n");
        System.out.printf("|        циклами вызова (в Mb)       |%n");
        System.out.printf("|------------------------------------|%n");
        System.out.printf("| %-6s | %-6s | %-6s | %-6s  |%n", "Total", "Free", "Diff", "Max");
    }

    /**
     * Отобразить дамп памяти JVM.
     */
    private void showMemoryDump() {
        long totalMemory = bytesToMegabytes(runtime.totalMemory());
        long freeMemory = bytesToMegabytes(runtime.freeMemory());
        long maxMemory = bytesToMegabytes(runtime.maxMemory());

        System.out.printf("| %-6s | %-6s | %-6s | %-6s  |%n", totalMemory, freeMemory, totalMemory - freeMemory, maxMemory);
    }

    /**
     * Преобразовать бмйты в мегабайты.
     * @param bytes количество байт
     * @return количество мегабайт
     */
    private long bytesToMegabytes(long bytes) {
        return bytes / MEGABYTE;
    }

    /**
     * Отобразить сообщение с преобразованием времени.
     * @param message cообщение
     * @param nanoseconds время в наносекундах
     */
    private void showMessage(String message, long nanoseconds) {
        long milliseconds = TimeUnit.NANOSECONDS.toMillis(nanoseconds);
        double seconds = (double) milliseconds / 1000L;
        System.out.printf("| %-20s | %6s сек. | %8s мс. | %12s нс.%n", message, seconds, milliseconds, nanoseconds);
    }

    /**
     * Запустить Garbage Collector.
     */
    private void runGagbageCollector() {
        runtime.gc();
    }

    @FunctionalInterface
    public interface JobSupplier {
        void doIt();
    }
}
